Il dataset lo si può trovare al seguente link: https://www.kaggle.com/datasets/shashwatwork/cerebral-stroke-predictionimbalaced-dataset. Di seguito sono presenti le colonne, con il loro relativo tipo di dato, del nostro dataset.
Come prime idee, abbiamo avuto le seguenti domande:
Dopodiché abbiamo evoluto l'analisi, tralasciando anche un po' le domande iniziali che avevamo. Le analisi che abbiami fatto sono le seguenti:
Come prima cosa importiamo i dati.
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
import plotly.figure_factory as ff
df = pd.read_csv("data/DatasetProgetto.csv")
print("Esempio dei dati contenuti nel csv")
df.head()
Esempio dei dati contenuti nel csv
| id | gender | age | hypertension | heart_disease | ever_married | work_type | Residence_type | avg_glucose_level | bmi | smoking_status | stroke | |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 0 | 46136 | Male | 14.0 | 0 | 0 | No | Never_worked | Rural | 161.28 | 19.1 | NaN | 0 |
| 1 | 16523 | Female | 8.0 | 0 | 0 | No | Private | Urban | 110.89 | 17.6 | NaN | 0 |
| 2 | 30669 | Male | 3.0 | 0 | 0 | No | children | Rural | 95.12 | 18.0 | NaN | 0 |
| 3 | 30468 | Male | 58.0 | 1 | 0 | Yes | Private | Urban | 87.96 | 39.2 | never smoked | 0 |
| 4 | 56543 | Female | 70.0 | 0 | 0 | Yes | Private | Rural | 69.04 | 35.9 | formerly smoked | 0 |
print("Informazioni utili:")
df.info()
#si può notare che ci sono 201 valori null per la colonna bmi
Informazioni utili: <class 'pandas.core.frame.DataFrame'> RangeIndex: 43400 entries, 0 to 43399 Data columns (total 12 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 id 43400 non-null int64 1 gender 43400 non-null object 2 age 43400 non-null float64 3 hypertension 43400 non-null int64 4 heart_disease 43400 non-null int64 5 ever_married 43400 non-null object 6 work_type 43400 non-null object 7 Residence_type 43400 non-null object 8 avg_glucose_level 43400 non-null float64 9 bmi 41938 non-null float64 10 smoking_status 30108 non-null object 11 stroke 43400 non-null int64 dtypes: float64(3), int64(4), object(5) memory usage: 4.0+ MB
Come primo dato ci interessa sapere quanti uomini/donne/altro sono presenti nel dataset.
px.histogram(df, x="gender", color="gender",
barmode="overlay",
title="Males/Females quantity",
height=500,
color_discrete_sequence=["blue", "red", "green"],
text_auto=True,
labels={'gender': 'Gender'}).update_layout(yaxis_title="Count")
Ci sono solo 11 "Other" e, visto che sul totale il loro impatto sarebbe irrelevante e sarebbero scomodi da gestire come terza variabile per il genere, decidiamo di toglierli.
df = df.loc[df["gender"] != "Other"]
Diamo un'occhiama a come l'età è distribuita tra maschi e femmine, si può notare che, come visto in precedenza, il numero di donne è nettamente maggiore rispetto al numero di uomini. La maggior parte delle persone hanno un range di età compreso tra 20 e 60, poi c'è un crescendo intorno agli ottant'anni.
df.loc[df["stroke"] == 0, "stroke"] = "NotHad"
df.loc[df["stroke"] == 1, "stroke"] = "Had"
agesChart = px.histogram(df, x="age",
color="gender",
barmode="overlay",
title="Age distribuition",
marginal="box",
height=500,
color_discrete_sequence=["blue", "red"],
opacity=.3,
labels={'age': 'Age', "gender": "Gender"}).update_layout(yaxis_title="Count")
agesChart.show()
strokeChart = px.histogram(df, x="age",
color="stroke",
barmode="overlay",
title="Age distribution of stroke",
marginal="box",
color_discrete_sequence=["green", "orange"],
opacity=.8,
height=500,
labels={'age': 'Age', "stroke": "Stroke"}).update_layout(yaxis_title="Count")
strokeChart.show()
C'é una donna (bimba) che ha avuto un ictus a 1.3 anni. Nonostante sia possibile avere un ictus così giovani decidiamo di mettere l'eta minima a 10 anni, dato che prima lo stile di vita non influisce sul fattore ictus. Salviamo comunque i dati delle persone con meno di 10 anni per un analisi futura.
dfUnder10 = df.loc[df["age"] < 10]
df = df.loc[df["age"] >= 10]
dfMales = df.loc[df["gender"] == "Male"]
dfFemales = df.loc[df["gender"] == "Female"]
dfSingle = df.loc[df["ever_married"] == "No"]
dfMarried = df.loc[df["ever_married"] == "Yes"]
Abbiamo rimosso "Others" dalla colonna "Gender", e abbiamo rimosso le persone con età inferiore a 10 anni, poiché abbiamo notato che entrambi questi dati erano superflui. Di seguito sono presenti il totale dei dati che abbiamo tenuto, suddivisi per genere.
fig = go.Figure(go.Pie(
values = [dfMales["gender"].count(), dfFemales["gender"].count()],
labels = ["Males", "Females"],
texttemplate = "%{label}: %{value} <br>(%{percent})",
textposition = "inside",
textfont=dict(
family="sans serif",
size=24,
color="white"
),
))
fig.update_layout(
width=600,
height=600,
)
fig.update_layout(
title={
'text': "Males/Females percentages",
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'})
fig.update_traces(marker=dict(colors=['blue', 'red']))
fig.show()
Come prima cosa possiamo notare che il numero di persone sposate è nettamente maggiore rispetto al numero di persone single.
px.histogram(df, x="ever_married", color="ever_married", barmode="overlay",
title="Quantità uomini/donne", height=500,
color_discrete_sequence=["purple", "darkorange"], text_auto=True)
fig = go.Figure(go.Pie(
values = [dfSingle["ever_married"].count(), dfMarried["ever_married"].count()],
labels = ["Single", "Married"],
texttemplate = "%{label}: %{value} <br>(%{percent})",
textposition = "inside",
textfont=dict(
family="sans serif",
size=24,
color="white"
)
))
fig.update_layout(
width=600,
height=600,
)
fig.update_layout(
title={
'text': "Married/Single percentages",
'y':0.9,
'x':0.5,
'xanchor': 'center',
'yanchor': 'top'})
colors =["purple", "darkorange"]
fig.update_traces(marker=dict(colors=colors))
fig.show()
grouped_df = df.groupby(["gender", "ever_married"])["stroke"].value_counts(normalize=True)*100
grouped_df = grouped_df.unstack()["Had"].reset_index()
grouped_df.loc[grouped_df["ever_married"] == "Yes", "ever_married"] = "Married"
grouped_df.loc[grouped_df["ever_married"] == "No", "ever_married"] = "Single"
grouped_df["name"] = grouped_df["ever_married"] + " " + grouped_df['gender'].astype(str)
fig = px.histogram(grouped_df, y="name", x="Had",
color="name",
color_discrete_sequence=["red", "red", "blue", "blue"],
title="Stroke rate by gender and marital status",
labels={
"name": "", "Had" : ""
},
text_auto= ".3f",
height=500
)
fig.update_layout(showlegend=False)
fig.update_layout(yaxis_title="")
fig.update_layout(xaxis_title="Stroke percentage")
fig.show()
La conclusione che possiamo trarre guardando questo grafico, è che il matrimonio sembra aumentare il rischio di avere un ictus, ma il tutto è fatto con dei dati che rappresentano una maggioranza di persone sposate. Quindi non è improbabile che questa rappresentazione sia dovuta solo dal fatto che un dato sovrasta l'altro.
fig = px.box(df, x="age",
color="ever_married",
color_discrete_sequence=["purple", "darkorange"],
title="Age distribution by marital status",
labels={"ever_married": "Ever married", "age": "Age"},
height=500
)
fig.show()
Facciamo anche una distrubuzione dell'età in base allo stato coniugale. Si nota che le persone sposate (quindi quelle con più ictus) sono anche quelle mediamente più anziane. Decidiamo quindi di fare una media di ictus in base all'eta e lo stato coniugale per visualizzare meglio se il matrimonio può essere un fattore di rischio.
mydf = df.copy()
mydf.loc[mydf["stroke"] == "Had", "stroke"] = 1
mydf.loc[mydf["stroke"] == "NotHad", "stroke"] = 0
mydf = mydf.groupby(["age", "ever_married"])["stroke"].mean()
mydf = mydf.reset_index(name = "values")
fig = px.scatter(mydf, x = "age",
y = "values",
color="ever_married",
trendline_options=dict(window=10),
title="Average number of strokes by age and marital status",
trendline="rolling",
height=500,
color_discrete_sequence=["purple", "darkorange"],
labels={"ever_married": "Ever married", "age": "Age"})
fig.update_yaxes(range=[-0.01, 0.11])
fig.update_xaxes(range=[20, 82])
fig.update_layout(yaxis_title="Avg(10) stroke percentage")
fig.data = [t for t in fig.data if t.mode == "lines"]
fig.update_traces(showlegend=True)
fig.show()
Questo ultimo grafico, mostra la media di ictus avuti in base all'età e allo stato coniugale. Si può notare chiaramente che il fattore che fa aumentare notevolmente il rischio di ictus è l'età in primis, e come secondo, a differenza di quello che potevamo notare prima, è che in realtà essere single aumenta anche se di poco, la possibilità di avere un ictus.
Di seguito è presente un primo grafico in cui vengono divisi secondo il loro stato da fuamtore.
smokersHistogram = px.histogram(df, x="smoking_status",
color="smoking_status",
title = "Smokers count",
color_discrete_sequence=["green", "red", "blue"],
text_auto= True,
labels={"smoking_status": "Smoking status"},
height=500
)
smokersHistogram.update_layout(yaxis_title="Count")
smokersHistogram.show()
groupedSmokers = df.groupby(["smoking_status"])["stroke"].value_counts(normalize=True)*100
groupedSmokers = groupedSmokers.unstack()["Had"].reset_index()
fig = px.histogram(groupedSmokers, y="smoking_status", x="Had",
color="smoking_status",
title="Stroke rate according to smoking status",
text_auto= ".3f",
labels={"smoking_status": "Smoking status"},
height=500
)
fig.update_layout(showlegend=False)
fig.update_layout(xaxis_title="Stroke percentage")
fig.update_layout(yaxis_title="")
fig.show()
Sembra che il se si fuma si ha la stessa probabilità di ictus di chi non ha mai fumato. E smettendo di fumare questa probabilità aumenta.
px.box(df, x="age",
color="smoking_status",
title="Age distribution according to smoking status",
color_discrete_sequence=["green", "red", "blue"],
labels={"smoking_status": "Smoking status", "age": "Age"},
height=500
)
Anche da questa analisi, come per il matrimonio si nota che le persone che hanno smesso di fumare sono quelle più anziane, ed é quindi logico che siano quelli con probabilità più alta
mydf = df.copy()
mydf.loc[mydf["stroke"] == "Had", "stroke"] = 1
mydf.loc[mydf["stroke"] == "NotHad", "stroke"] = 0
mydf = mydf.groupby(["age", "smoking_status"])["stroke"].mean()
mydf = mydf.reset_index(name = "values")
graph = px.scatter(mydf, x = "age",
y = "values",
color="smoking_status",
trendline_options=dict(window=10),
title="Average number of strokes according to age and smoking status",
trendline="rolling",
height=500,
labels={"smoking_status": "Smoking status", "age": "Age", "values": "Avg(10) stroke percentage"}
)
graph.data = [t for t in graph.data if t.mode == "lines"]
graph.update_traces(showlegend=True)
graph.update_yaxes(range=[-0.01, 0.1])
graph.update_xaxes(range=[20, 82])
graph.show()
Come si può notare in questo ultimo grafico, è evidente che, oltre all'età, il fumo è effettivamente un fattore a rischio. Soprattutto dai cinquanta anni in avanti c'è una crescita maggiore per chi fuma, e chi ha fumato, mentre chi non ha mai fumato rimane sempre inferiore agli altri due. Un altra cosa che si può notare è che dopo gli ottant'anni, le persone che non hanno mai fumato hanno una crescita maggiore rispetto le altre, e qua facciamo la supposizione che gran parte degli altri siano già deceduti e che quindi la media loro diminuisce.
Come prima analisi guardiamo come sono correlati il BMI e il livello di glucosio con l'età che avanza.
bmiChart = px.box(df, x="age",
y="bmi",
title="Correlation of BMI with age",
points=False,
height=500,
labels={"bmi": "BMI", "age": "Age"}
)
bmiChart.update_yaxes(range=[17, 37])
bmiChart.show()
glucoseChart = px.box(df, x="age",
y="avg_glucose_level",
title="Correlation of glucose level with age",
points=False,
height=500,
labels={"avg_glucose_level": "Avg. glucose", "age": "Age"}
)
glucoseChart.update_yaxes(range=[70, 190])
glucoseChart.show()
Notiamo subito che il BMI ha un crescendo intorno ai 50/60 anni per poi calare di nuovo. Come per il fumo, supponiamo che sia perché le persone con una massa maggiore, quindi vita poco sana, decedano prima. Notiamo che il livello di glucosio inizia a salire intorno ai 55 anni e continua a crescere.
Facciamo una mappa per mettere insieme il livello di glucosio e il BMI, per vedere come essi sono correlati con l'avere un ictus o meno.
df = df.sort_values(by=['age'])
bmiGlugoseScatter = px.scatter(df, x="avg_glucose_level",
y="bmi", color="stroke" ,
opacity = .8,
title="BMI map and glucose level correlated with stroke",
height=500,
color_discrete_sequence=["green", "orange"],
labels={"avg_glucose_level": "Avg. glucose", "bmi": "BMI", "stroke": "Stroke"}
)
bmiGlugoseScatter.show()
bmiGlugoseDensity = px.density_contour(df, x="avg_glucose_level",
y="bmi", color="stroke",
title="BMI and glucose level groups correlated with stroke",
height=500,
color_discrete_sequence=["green", "orange"],
labels={"avg_glucose_level": "Avg. glucose", "bmi": "BMI", "stroke": "Stroke"}
)
bmiGlugoseDensity.update_xaxes(range=[40, 260])
bmiGlugoseDensity.update_yaxes(range=[11, 55])
bmiGlugoseDensity.show()
Dai grafici si puo notare che il bmi rimane stabile con l'età, ma i livelli di glucosio aumentano con l'aumentare degli anni. Nello scatter plot dove si mettono in relazione il bmi con i livelli di glucosio, si possono notare due gruppi di persone con l'ictus, uno sulla destra e uno sulla sinistra. Nell'ultimo grafico questi due gruppi si notano bene e si nota che il gruppo "destra", quindi quelli con i livelli di glucosio più alto sono completamente separati da quelli che non hanno avuto un ictus. Nel secondo grafico è rappresentata la mappa di densità, e si può notare che risaltano in maniera particolare due zone. Decidiamo quindi di fare una mappa, dividendo il BMI e i livelli di glucosio in gruppi per vedere quali sono quelli a rischio. Divisione fatta per il BMI:
Divisione fatta per il livello di glucosio:
mydf =df[["bmi", "avg_glucose_level", "stroke"]]
mydf['BMIstatus'] = pd.cut(mydf['bmi'],
bins=[0, 18.5, 25, 30, float('Inf')],
labels=['Underweight', 'Standard weight', 'Overweight', 'Obesity'])
mydf['GlucoseStatus'] = pd.cut(mydf['avg_glucose_level'],
bins=[0, 70, 125, 200, float('Inf')],
labels=['Ipoglicemia', 'Normale', 'Alto', 'Molto alto'])
mydf = (mydf.groupby(["BMIstatus", "GlucoseStatus"])["stroke"].value_counts(normalize=True)*100).round(2)
mydf = mydf.unstack()["Had"].reset_index()
final = [ mydf["Had"].values[i * 4:(i + 1) * 5] for i in range((len( mydf["Had"].values) + 5 - 1) // 5 )]
fig = go.Figure(data=go.Heatmap(
z=final,
y=['Underweight', 'Standard weight', 'Overweight', 'Obesity'],
x=['Hypoglycaemia', 'Normal', 'High', 'Very high'],
hoverongaps = False,
texttemplate="%{z}\u0025"))
fig.update_layout(title = "Stroke rates based on bmi and glucose levels", height=500)
fig.show()
Dal grafico sovrastante si può notare in maniera chiara ed efficace quali valori tra bmi e livello di glucosio, causino un aumento della possibilità di avere un ictus.
Quello che abbiamo fatto è stato una semplice distribuizione dell'età, e abbiamo stabilito che per il tempo che ci è stato dato non è possibile approfondire un l'analisi guardando anche l'ipertensione e i problemi al cuore.
df.loc[df["hypertension"] == 0, "hypertension"] = "NotHad"
df.loc[df["hypertension"] == 1, "hypertension"] = "Had"
hypertensionChart = px.histogram(df, x="age", color="hypertension", barmode="overlay",
title="Age distribution of hypertension",
marginal="box",
labels={"age": "Age", "hypertension": "Hypertension"},
height = 500
)
hypertensionChart.update_layout(yaxis_title="Count")
hypertensionChart.show()
df.loc[df["heart_disease"] == 0, "heart_disease"] = "NotHad"
df.loc[df["heart_disease"] == 1, "heart_disease"] = "Had"
heartChart = px.histogram(df, x="age",
color="heart_disease",
barmode="overlay",
title="Age distribution of heart disease",
marginal="box",
labels={"age": "Age", "heart_disease": "Heart disease"},
height = 500
)
heartChart.update_layout(yaxis_title="Count")
heartChart.show()
workHistogram = px.histogram(df, x="work_type",
color="stroke",
barmode="group",
title="Type of stroke-related work",
color_discrete_sequence=["green", "orange"],
labels={"stroke": "Stroke", "work_type": "Work type"},
height=500
)
workHistogram.update_layout(yaxis_title="Count")
workHistogram.show()
Le persone sembrano preferire il lavoro in aziende private, mentre il numero di lavoratori autonomi/lavori pubblici sembra essere simile (i bambini possono essere ignorati). Mentre i disoccupati sono molto pochi.
I dipendenti privati sembrano soffrire di ictus più di altri tipi di lavoro (forse a causa della pressione lavorativa).
mydf = df.copy()
mydf = mydf.filter(items=["work_type", "stroke"])
mydf["hadStroke"] = mydf["stroke"] == "Had"
mydf = mydf.groupby(["work_type"])["hadStroke"].mean()
mydf = mydf.reset_index(name = "mean")
fig = px.histogram(mydf,
x="work_type",
y="mean",
title="Stroke rate by type of work",
labels={'count':'', "work_type": "Work type"},
height=500
)
fig.update_traces(text= [f'{val:.3f}\u0025' for val in mydf['mean']])
fig.update_yaxes(visible=False, showticklabels=False)
fig.show()
Normalizzando i valori si nota però che invece sono le persone con un lavoro indipendente ad avere il valore di ictus più alto. Mentre i dipendenti privati e i dipendenti governativi, hanno un valore molto simile.
Per concludere questo percorso, si può affermare che alcuni fattori scatenanti per avere un ictus possono essere:
Quello che è stato evidente, è che tra questi fattori l'avanzare dell'età è il più scatenante.